Skip to content

Implement tom-select for related documents #6610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 93 additions & 59 deletions kitsune/sumo/static/sumo/js/wiki_search.js
Original file line number Diff line number Diff line change
@@ -1,82 +1,116 @@
import Search from "sumo/js/search_utils";
import TomSelect from "tom-select";

import "sumo/tpl/wiki-related-doc.njk";
import "sumo/tpl/wiki-search-results.njk";
import nunjucksEnv from "sumo/js/nunjucks"; // has to be loaded after templates

(function($) {
var searchTimeout;
var locale = $('html').attr('lang');
document.addEventListener("DOMContentLoaded", function() {
const locale = document.documentElement.lang;
const search = new Search(`/${locale}/search`, { w: 1, format: 'json' });

var $searchField = $('#search-related');
var $relatedDocsList = $('#related-docs-list');
var $resultsList;
const relatedDocsList = document.getElementById('related-docs-list');
const searchInput = document.getElementById('search-related');

// To search for only wiki articles we pass w=1
var search = new Search('/' + locale + '/search', {w: 1, format: 'json'});
if (!searchInput || !relatedDocsList) {
return;
}

function createResultsList() {
$resultsList = $('<div />').addClass('input-dropdown');
$searchField.after($resultsList);
$resultsList.css('width', $searchField.outerWidth());
$resultsList.show();
if (document.body.classList.contains('edit_metadata') || document.body.classList.contains('new')) {
relatedDocsList.style.display = 'none';
}

$resultsList.on('click', '[data-pk]', function() {
var $this = $(this);
let currentDocId = null;
const documentForm = document.querySelector('form[data-document-id]');
if (documentForm) {
currentDocId = documentForm.dataset.documentId;
}

$relatedDocsList.children('.empty-message').remove();
const tomSelect = new TomSelect(searchInput, {
valueField: 'id',
labelField: 'title',
searchField: 'title',
create: false,
closeAfterSelect: true,
maxItems: null, // Allow multiple selections
plugins: {
remove_button: {
title: 'Remove this document'
}
},
load: function(query, callback) {
if (!query.length) {
return callback();
}

if (!$relatedDocsList.children('[data-pk=' + $this.data('pk') + ']').length) {
var context = {
name: 'related_documents',
search.query(query, function(data) {
if (!data || !data.results) {
return callback();
}

const formattedResults = data.results
.filter(result => result.type === 'document')
.map(item => {
let id = item.id;
if (!id && item.url) {
const match = item.url.match(/\/(\d+)\//);
if (match) {
id = match[1];
}
}
return {
id: id,
title: item.title,
url: item.url
};
})
.filter(item => item.id)
.filter(item => !currentDocId || item.id != currentDocId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be !==


callback(formattedResults);
});
},
onItemAdd: function(value, item) {
const emptyMessage = relatedDocsList.querySelector('.empty-message');
if (emptyMessage) {
emptyMessage.remove();
}
// Note: The actual list item is added in render.item
},
render: {
option: function(item, escape) {
return `<div>${escape(item.title)}</div>`;
},
// Renders the selected item in the input field and triggers adding the item visually to the list below.
item: function(item, escape) {
const context = {
name: 'related_documents', // Input field name prefix
doc: {
id: $this.data('pk'),
title: $this.text()
id: item.id,
title: item.title
}
};
relatedDocsList.insertAdjacentHTML('beforeend', nunjucksEnv.render('wiki-related-doc.njk', context));

$relatedDocsList.append(nunjucksEnv.render('wiki-related-doc.njk', context));
}
});
}

function showResults(data) {
if (!$resultsList) {
createResultsList();
}
$resultsList.html(nunjucksEnv.render('wiki-search-results.njk', data));
}

function handleSearch() {
var $this = $(this);
if ($this.val().length === 0) {
window.clearTimeout(searchTimeout);
if ($resultsList) {
$resultsList.html('');
$resultsList.hide();
// This div is what TomSelect displays in the input field for the selected item
return `<div>${escape(item.title)}</div>`;
},
no_results: function(data, escape) {
const noDocsFound = typeof gettext !== 'undefined' ? gettext('No documents found') : 'No documents found';
return `<div class="no-results">${noDocsFound}</div>`;
}
} else if ($this.val() !== search.lastQuery) {
window.clearTimeout(searchTimeout);
searchTimeout = window.setTimeout(function () {
search.query($this.val(), showResults);
}, 200);
}
}

$searchField.on('keyup', handleSearch);
});

$searchField.on('focus', function() {
if ($resultsList) {
$resultsList.show();
tomSelect.on('item_remove', function(value) {
const itemToRemove = relatedDocsList.querySelector(`[data-pk="${value}"]`);
if (itemToRemove) {
itemToRemove.remove();
}
});

$searchField.on('blur', function() {
if ($resultsList) {
// We use a timeout to ensure that you can still click on the dropdown
window.setTimeout(function() {
$resultsList.hide();
}, 100);
if (relatedDocsList.children.length === 0) {
const noRelatedDocs = typeof gettext !== 'undefined' ? gettext('No related documents.') : 'No related documents.';
relatedDocsList.innerHTML = `<div class="empty-message">${noRelatedDocs}</div>`;
}
});
})(jQuery);
});
6 changes: 6 additions & 0 deletions kitsune/sumo/static/sumo/scss/components/_wiki.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

@import 'tom-select/dist/scss/tom-select.default';

// Hide the related documents list on edit metadata and new document pages
body.edit_metadata #related-docs-list,
body.new #related-docs-list {
display: none !important;
}

figure {
&.linked-in-product {
display: flex;
Expand Down
34 changes: 18 additions & 16 deletions kitsune/wiki/jinja2/wiki/includes/document_macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -416,23 +416,25 @@ <h2 class="sumo-page-subheading">{{ _('Related Articles') }}</h2>
<div class="sumo-card-grid">
<div class="scroll-wrap">
{% for related in docs %}
<div class="card card--article">
<img class="card--icon-sm" src="{{ webpack_static('protocol/img/icons/reader-mode.svg') }}" alt="todo: title" />
<div class="card--details">
<h3 class="card--title">
<a class="expand-this-link" href="{{ url('wiki.document', related.slug) }}"
data-event-name="link_click"
data-event-parameters='{"link_name": "related.kb-article"}'>
{{ related.title }}
</a>
</h3>
<div class="card--desc">
<p>
{{ related.html|striptags|truncate(length=150) }}
</p>
</div>
{% if related.is_unrestricted_for(user) %}
<div class="card card--article">
<img class="card--icon-sm" src="{{ webpack_static('protocol/img/icons/reader-mode.svg') }}" alt="todo: title" />
<div class="card--details">
<h3 class="card--title">
<a class="expand-this-link" href="{{ url('wiki.document', related.slug) }}"
data-event-name="link_click"
data-event-parameters='{"link_name": "related.kb-article"}'>
{{ related.title }}
</a>
</h3>
<div class="card--desc">
<p>
{{ related.html|striptags|truncate(length=150) }}
</p>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
Expand Down
18 changes: 8 additions & 10 deletions kitsune/wiki/jinja2/wiki/includes/related_docs_widget.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<input type="search" autocomplete="off" id="search-related" placeholder="{{ _('Search for documents...') }}">
<ul id="related-docs-list">
<select id="search-related" class="tom-select-related-docs" placeholder="{{ _('Search for documents...') }}">
{% if related_documents.count() %}
{% for doc in related_documents.all() %}
<li data-pk="{{ doc.pk }}">
<input type="checkbox" name="{{ name }}" value="{{ doc.pk }}" checked/>
{{ doc.title }}
<span data-close-type="remove" class="close-button"></span>
</li>
<option value="{{ doc.pk }}" selected>{{ doc.title }}</option>
{% endfor %}
{% else %}
<li class="empty-message">{{ _('No related documents.') }}</li>
{% endif %}
</ul>
</select>
<div id="related-docs-list">
{% if not related_documents.count() %}
<div class="empty-message">{{ _('No related documents.') }}</div>
{% endif %}
</div>